﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Text;
using System.Collections.ObjectModel;

namespace Calhoon.MarkupParser
{
	[Flags]
	public enum ExpectedPatterns
	{
		None = 0,
		ExpectOpeningTag = 2,
		ExpectClosingTag = 4,
		ExpectIdentifierOpening = 8,
		ExpectDigit = 16,
		ExpectNumber = 32,
		ExpectCommentOpeningTag = 64,
		ExpectCommentClosingTag = 128
	}

	public class Parser
	{
		protected virtual string OpeningSequence {
			get {
				return "<";
			}
		}

		protected virtual string ClosingSequence {
			get {
				return ">";
			}
		}

		protected virtual string CommentOpeningSequence {
			get {
				return "<!--";
			}
		}

		protected virtual string CommentClosingSequence {
			get {
				return "-->";
			}
		}

		private ParseState mState;
		protected ParseState State {
			get {
				return mState;
			}
		}

		public string Source {
			get {
				return mState.Source;
			}
			set {
				mState.Source = value;
			}
		}

		public Collection<PrimitiveTag> Tags {
			get {
				return mState.Document;
			}
		}

		public Parser(ParseState state) {
			mState = state;
		}

		protected virtual bool CheckSequence(string seq) {
			if (mState.Peek(seq.Length) == seq) {
				return true;
			}
			else {
				return false;
			}
		}

		protected virtual bool CheckOpeningSequence() {
			return CheckSequence(OpeningSequence);
		}

		protected virtual bool CheckClosingSequence() {
			return CheckSequence(ClosingSequence);
		}

		protected virtual bool CheckCommentOpeningSequence() {
			if (!string.IsNullOrEmpty(CommentOpeningSequence)) {
				return CheckSequence(CommentOpeningSequence);
			}
			else {
				return false;
			}
		}

		protected virtual bool CheckCommentClosingSequence() {
			if (!string.IsNullOrEmpty(CommentClosingSequence)) {
				return CheckSequence(CommentClosingSequence);
			}
			else {
				return false;
			}
		}

		protected virtual void ConsumeSequence(string seq) {
			if (CheckSequence(seq)) {
				mState.Advance(seq.Length);
			}
		}

		protected virtual void ConsumeOpeningSequence() {
			ConsumeSequence(OpeningSequence);
		}

		protected virtual void ConsumeClosingSequence() {
			ConsumeSequence(ClosingSequence);
		}

		protected virtual void ConsumeCommentOpeningSequence() {
			ConsumeSequence(CommentOpeningSequence);
		}

		protected virtual void ConsumeCommentClosingSequence() {
			ConsumeSequence(CommentClosingSequence);
		}

		protected virtual void EatUnexpected(ExpectedPatterns patterns, params string[] expect) {
			mState.EatUnexpected(CheckExpected, (int)patterns, expect);
		}

		protected bool CheckExpected(ParseState state, int patterns) {
			bool result = false;
			if ((patterns & (int)ExpectedPatterns.ExpectDigit) != 0) {
				result = result || char.IsDigit(state.Peek());
			}

			if ((patterns & (int)ExpectedPatterns.ExpectNumber) != 0) {
				result = result || char.IsNumber(state.Peek());
			}

			if ((patterns & (int)ExpectedPatterns.ExpectIdentifierOpening) != 0) {
				result = result || IsIdentifierStart(state.Peek());
			}

			if ((patterns & (int)ExpectedPatterns.ExpectOpeningTag) != 0) {
				result = result || (state.Peek(OpeningSequence.Length) == OpeningSequence);
			}

			if ((patterns & (int)ExpectedPatterns.ExpectClosingTag) != 0) {
				result = result || (state.Peek(ClosingSequence.Length) == ClosingSequence);
			}

			if ((patterns & (int)ExpectedPatterns.ExpectCommentOpeningTag) != 0) {
				result = result || (state.Peek(CommentOpeningSequence.Length) == CommentOpeningSequence);
			}

			if ((patterns & (int)ExpectedPatterns.ExpectCommentClosingTag) != 0) {
				result = result || (state.Peek(CommentClosingSequence.Length) == CommentClosingSequence);
			}

			return result;
		}

		public virtual Collection<PrimitiveTag> Parse() {
			mState.Document.Clear();
			var currentLiteral = new StringBuilder();

			while (!mState.EndOfFile) {
				if (CheckOpeningSequence() || CheckCommentOpeningSequence()) {
					if (currentLiteral.Length > 0) {
						mState.Document.Add(new LiteralPrimitiveTag(currentLiteral.ToString()));
						currentLiteral = new StringBuilder();
					}

					if (CheckCommentOpeningSequence()) {
						CommentPrimitiveTag newComment = ParseComment();
						mState.Document.Add(newComment);
					}
					else if (CheckOpeningSequence()) {
						PrimitiveTag newTag = ParseTag();
						mState.Document.Add(newTag);
					}
				}
				else {
					currentLiteral.Append(mState.Advance());
				}
			}

			if (currentLiteral.Length > 0) {
				mState.Document.Add(new LiteralPrimitiveTag(currentLiteral.ToString()));
			}

			return mState.Document;
		}

		protected virtual CommentPrimitiveTag ParseComment() {
			CommentPrimitiveTag newComment = new CommentPrimitiveTag();
			int startIndex = mState.Index;
			StringBuilder text = new StringBuilder();

			ConsumeCommentOpeningSequence();

			while (!mState.EndOfFile && !CheckCommentClosingSequence()) {
				text.Append(mState.Peek());
				mState.Advance();
			}

			if (!mState.EndOfFile) {
				ConsumeCommentClosingSequence();
			}

			newComment.Text = text.ToString();
			newComment.RawText = mState.Source.Substring(startIndex, mState.Index - startIndex);
			return newComment;
		}

		protected virtual PrimitiveTag ParseTag() {
			PrimitiveTag newTag = new PrimitiveTag();
			newTag.TagName = string.Empty;
			int startIndex = mState.Index;

			ConsumeOpeningSequence();
			mState.EatWhiteSpace();
			EatUnexpected(ExpectedPatterns.ExpectClosingTag | ExpectedPatterns.ExpectCommentOpeningTag | ExpectedPatterns.ExpectIdentifierOpening, "/");

			//Closing flag.
			if (!mState.EndOfFile && !CheckCommentOpeningSequence()) {
				if (mState.Peek() == '/') {
					newTag.IsClosing = true;
					mState.Advance();
				}
				mState.EatWhiteSpace();
				EatUnexpected(ExpectedPatterns.ExpectIdentifierOpening | ExpectedPatterns.ExpectClosingTag | ExpectedPatterns.ExpectCommentOpeningTag);
			}

			//Tag name.
			if (!mState.EndOfFile && !CheckCommentOpeningSequence()) {
				newTag.TagName = ParseIdentifier();
				mState.EatWhiteSpace();
			}

			//Attributes.
			if (!mState.EndOfFile && !CheckCommentOpeningSequence()) {
				Dictionary<string, AttributeValue> attributes = ParseAttributes();
				if (attributes != null) {
					foreach (KeyValuePair<string, AttributeValue> attribute in attributes) {
						newTag.Attributes.Add(attribute.Key, attribute.Value);
					}
				}

				mState.EatWhiteSpace();
				EatUnexpected(ExpectedPatterns.ExpectClosingTag | ExpectedPatterns.ExpectCommentOpeningTag, "/");
			}

			//Self closing flag.
			if (!mState.EndOfFile && !CheckCommentOpeningSequence()) {
				if (mState.Peek() == '/') {
					newTag.IsSelfClosing = true;
					mState.Advance();
				}

				mState.EatWhiteSpace();
				EatUnexpected(ExpectedPatterns.ExpectClosingTag | ExpectedPatterns.ExpectCommentOpeningTag);
			}

			if (CheckClosingSequence()) {
				ConsumeClosingSequence();
			}

			newTag.RawText = mState.Source.Substring(startIndex, mState.Index - startIndex);
			return newTag;
		}

		protected virtual Dictionary<string, AttributeValue> ParseAttributes() {
			Dictionary<string, AttributeValue> attributes = new Dictionary<string, AttributeValue>();

			while (!mState.EndOfFile && !CheckClosingSequence() && !CheckCommentOpeningSequence() && mState.Peek() != '/') {
				AttributeDefinition attribute = ParseAttribute();
				if (attribute != null) {
					if (!attributes.ContainsKey(attribute.Key)) {
						attributes.Add(attribute.Key, attribute.Value);
					}
					else {
						attributes[attribute.Key] = attribute.Value;
					}
				}
				mState.EatWhiteSpace();
				EatUnexpected(ExpectedPatterns.ExpectClosingTag | ExpectedPatterns.ExpectCommentOpeningTag | ExpectedPatterns.ExpectIdentifierOpening, "/");
			}

			return attributes;
		}

		protected virtual AttributeDefinition ParseAttribute() {
			string attributeName = string.Empty;
			AttributeValue attributeValue;

			mState.EatWhiteSpace();
			EatUnexpected(ExpectedPatterns.ExpectIdentifierOpening | ExpectedPatterns.ExpectClosingTag | ExpectedPatterns.ExpectCommentOpeningTag, "/");

			attributeName = ParseIdentifier();
			mState.EatWhiteSpace();
			EatUnexpected(ExpectedPatterns.ExpectClosingTag | ExpectedPatterns.ExpectCommentOpeningTag, "=", "/");

			if (mState.EndOfFile || CheckClosingSequence() || CheckCommentOpeningSequence()) {
				return null;
			}

			if (mState.Peek() == '=') {
				mState.Advance();
				mState.EatWhiteSpace();
			}

			if (mState.EndOfFile || CheckClosingSequence() || CheckCommentOpeningSequence()) {
				return null;
			}

			attributeValue = ParseValue();

			return new AttributeDefinition(attributeName, attributeValue);
		}

		protected virtual string ParseIdentifier() {
			var identifier = new StringBuilder();

			if (mState.EndOfFile || !IsIdentifierStart(mState.Peek()) || CheckClosingSequence() || CheckCommentOpeningSequence()) {
				return identifier.ToString();
			}

			while ((!mState.EndOfFile && !CheckClosingSequence() && !CheckCommentOpeningSequence()) && (char.IsLetterOrDigit(mState.Peek()) || mState.Peek() == '_')) {
				identifier.Append(mState.Peek());
				mState.Advance();
			}

			return identifier.ToString();
		}

		protected virtual bool IsIdentifierStart(char character) {
			return character == '_' || char.IsLetter(character);
		}

		protected virtual AttributeValue ParseValue() {
			var value = new StringBuilder();
			AttributeValueDelimiterType delim = AttributeValueDelimiterType.None;

			if (mState.EndOfFile || CheckClosingSequence() || CheckCommentOpeningSequence()) {
				return new AttributeValue(string.Empty, AttributeValueDelimiterType.None);
			}

			char current = mState.Peek();
			switch (current) {
				case '"':
				case '\'':
					delim = (AttributeValueDelimiterType)current;
					mState.Advance();
					current = mState.Peek();
					break;
				default:
					delim = AttributeValueDelimiterType.None;
					break;
			}

			while (!mState.EndOfFile && !CheckCommentOpeningSequence()) {

				if (delim == AttributeValueDelimiterType.None && (mState.IsWhiteSpace(current) || CheckClosingSequence())) {
					break;
				}
				else if (delim != AttributeValueDelimiterType.None && current == (char)delim) {
					mState.Advance();
					break;
				}
				else {
					value.Append(mState.Advance());
				}

				if (!mState.EndOfFile) {
					current = mState.Peek();
				}
			}

			return new AttributeValue(value.ToString(), delim);
		}
	}
}